From c0ccb8ca5e8435a073a0ec54a3f366b3a6e80ef2 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Fri, 6 Jun 2025 18:55:54 +0200 Subject: [PATCH] improvement --- Containerfile | 3 + Gemfile | 1 + Gemfile.lock | 2 + README.md | 117 ++++++++++++++++++---- app.rb | 266 +++++++++++++++++++++++++++++++++++++++++--------- responses.yml | 192 ++++++++++++++++++++++++++++++++++++ 6 files changed, 517 insertions(+), 64 deletions(-) create mode 100644 responses.yml diff --git a/Containerfile b/Containerfile index 9681f94..873b2be 100644 --- a/Containerfile +++ b/Containerfile @@ -15,6 +15,9 @@ COPY Gemfile* ./ # Install Ruby gems RUN bundle install --without development +# Copy responses file first +COPY responses.yml ./ + # Copy application code COPY . . diff --git a/Gemfile b/Gemfile index b1fe56f..b2cfbb1 100644 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,7 @@ source 'https://rubygems.org' gem 'sinatra', '~> 3.0' gem 'puma', '~> 6.0' gem 'json', '~> 2.6' +gem 'yaml', '~> 0.2' group :development do gem 'rerun' diff --git a/Gemfile.lock b/Gemfile.lock index 5b52a12..ad0e1ce 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -38,6 +38,7 @@ GEM rack-protection (= 3.2.0) tilt (~> 2.0) tilt (2.6.0) + yaml (0.4.0) PLATFORMS aarch64-linux-gnu @@ -57,6 +58,7 @@ DEPENDENCIES puma (~> 6.0) rerun sinatra (~> 3.0) + yaml (~> 0.2) BUNDLED WITH 2.6.7 diff --git a/README.md b/README.md index 43cf30a..f992d94 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,104 @@ -# No as a Service (NaaS) +# No as a Service (NaaS) - Multilingual Edition -A simple Sinatra-based web service that provides creative "no" responses in both HTML and JSON formats. +A Sinatra-based web service that provides creative "no" responses in multiple languages, with full support for all Nordic languages and automatic language detection. ## Features +- **Multilingual Support**: 8 languages including all Nordic languages +- **Automatic Language Detection**: Via Accept-Language headers and URL parameters - **Dual Format Support**: Serves both HTML (for browsers) and JSON (for APIs) -- **Random Responses**: Returns a random "no" response from a curated list -- **Modern Styling**: Beautiful glassmorphism design for the HTML interface +- **Random Responses**: Returns a random "no" response from curated lists per language +- **Modern Styling**: Beautiful glassmorphism design with language indicators - **Health Checks**: Built-in health check endpoint for monitoring - **Containerized**: Ready for deployment with Podman/Docker +## Supported Languages + +- 🇬🇧 **English** (`en`) - Default +- 🇳🇴 **Norwegian** (`no`) - Norsk +- 🇸🇪 **Swedish** (`sv`) - Svenska +- 🇩🇰 **Danish** (`da`) - Dansk +- 🇮🇸 **Icelandic** (`is`) - Íslenska +- 🇫🇮 **Finnish** (`fi`) - Suomi +- 🇫🇴 **Faroese** (`fo`) - Føroyskt +- 🏔️ **Northern Sami** (`smi`) - Sámegiella + ## API Endpoints - `GET /` - Returns a random "no" (HTML by default, JSON with Accept header) -- `GET /api/no` - Explicit JSON endpoint with additional metadata +- `GET /api/no` - Explicit JSON endpoint with language detection +- `GET /api/no/:lang` - Get response in specific language (e.g., `/api/no/no`) +- `GET /languages` - List all available languages - `GET /health` - Health check endpoint - `GET /*` - Catch-all that returns "no" for any other path -## Content Negotiation +## Language Detection -The root endpoint (`/`) supports content negotiation: -- Browser requests get HTML -- Requests with `Accept: application/json` get JSON +The service automatically detects language preference through: + +1. **URL Parameter**: `?lang=no` (highest priority) +2. **Accept-Language Header**: Browser language preferences +3. **Default Fallback**: English (`en`) + +Language family mapping is supported: +- `nb`, `nn` → `no` (Norwegian variants) +- `sv-*` → `sv` (Swedish variants) +- `da-*` → `da` (Danish variants) +- etc. ## Usage Examples ### HTML (Browser) -``` +```bash +# Default language (auto-detected) curl http://localhost:4567/ + +# Specific language via parameter +curl http://localhost:4567/?lang=no + +# Language via Accept-Language header +curl -H "Accept-Language: sv-SE,sv;q=0.9" http://localhost:4567/ ``` -### JSON +### JSON API ```bash -# Using Accept header +# Auto-detected language curl -H "Accept: application/json" http://localhost:4567/ -# Using explicit API endpoint -curl http://localhost:4567/api/no +# Specific language endpoint +curl http://localhost:4567/api/no/is + +# With language parameter +curl http://localhost:4567/api/no?lang=fi + +# List available languages +curl http://localhost:4567/languages ``` -### Sample JSON Response +### Sample JSON Responses + +**Basic Response:** ```json { - "answer": "Absolutely not", + "answer": "Absolutt ikke", + "language": "no", + "language_name": "Norsk (Norwegian)", "timestamp": "2025-06-06T12:00:00Z", "service": "No as a Service", - "version": "1.0.0" + "version": "2.0.0" +} +``` + +**Languages List:** +```json +{ + "languages": [ + {"code": "en", "name": "English"}, + {"code": "no", "name": "Norsk (Norwegian)"}, + {"code": "sv", "name": "Svenska (Swedish)"} + ], + "default": "en", + "timestamp": "2025-06-06T12:00:00Z" } ``` @@ -101,6 +154,7 @@ The service includes a health check endpoint at `/health` that returns: ```json { "status": "healthy", + "languages_loaded": 8, "timestamp": "2025-06-06T12:00:00Z" } ``` @@ -110,11 +164,38 @@ The service includes a health check endpoint at `/health` that returns: ``` . ├── app.rb # Main Sinatra application +├── responses.yml # Multilingual response data ├── Gemfile # Ruby dependencies ├── Containerfile # Container build instructions └── README.md # This file ``` +## Adding New Languages + +To add a new language, edit `responses.yml`: + +```yaml +xx: # Language code + name: "Language Name" + responses: + - "Response 1" + - "Response 2" + # ... more responses +``` + +Then optionally add language family mapping in `app.rb` if needed. + +## Nordic Language Features + +Each Nordic language includes culturally appropriate expressions: +- **Norwegian**: "Når kua flyger" (When cows fly) +- **Swedish**: "När grisar flyger" (When pigs fly) +- **Danish**: "Når svin flyver" (When pigs fly) +- **Icelandic**: "Þegar svín fljúga" (When pigs fly) +- **Finnish**: "Kun lehmät lentää" (When cows fly) +- **Faroese**: "Tá svín fljúgva" (When pigs fly) +- **Northern Sami**: "Go vuonji liddjo" (When reindeer fly) + ## License -This project is released under the MIT License. Feel free to use it for your negative response needs! +This project is released under the MIT License. Perfect for all your multilingual negative response needs!. Feel free to use it for your negative response needs! diff --git a/app.rb b/app.rb index cb17f1a..fde1dd0 100644 --- a/app.rb +++ b/app.rb @@ -1,81 +1,180 @@ require 'sinatra' require 'json' +require 'yaml' # Configure Sinatra set :port, ENV['PORT'] || 4567 set :bind, '0.0.0.0' -# Array of creative "no" responses -NO_RESPONSES = [ - "No", - "Nope", - "Absolutely not", - "Not a chance", - "Never", - "No way", - "Negative", - "Not happening", - "Nah", - "No siree", - "Not today", - "Dream on", - "Over my dead body", - "When pigs fly", - "Not in a million years", - "Forget about it", - "No dice", - "Not on your life", - "Fat chance", - "No can do" -].freeze +# Load responses from YAML file +RESPONSES = YAML.load_file('responses.yml') +DEFAULT_LANGUAGE = 'en' -# Helper method to get a random "no" -def get_no_response - NO_RESPONSES.sample +# Helper method to get a random "no" response +def get_no_response(lang = DEFAULT_LANGUAGE) + language = RESPONSES[lang] || RESPONSES[DEFAULT_LANGUAGE] + language['responses'].sample +end + +# Helper method to get language name +def get_language_name(lang) + language = RESPONSES[lang] || RESPONSES[DEFAULT_LANGUAGE] + language['name'] +end + +# Helper method to get available languages +def get_available_languages + RESPONSES.map { |code, data| { code: code, name: data['name'] } } +end + +# Helper method to determine language from request +def determine_language(params, headers) + # Priority: URL parameter > Accept-Language header > default + return params[:lang] if params[:lang] && RESPONSES[params[:lang]] + + # Parse Accept-Language header + if headers['HTTP_ACCEPT_LANGUAGE'] + accepted_langs = headers['HTTP_ACCEPT_LANGUAGE'] + .split(',') + .map { |lang| lang.split(';').first.downcase.strip } + + # Check for exact matches first + accepted_langs.each do |lang| + return lang if RESPONSES[lang] + end + + # Check for language family matches (e.g., 'nb' or 'nn' -> 'no') + accepted_langs.each do |lang| + case lang + when /^nb|nn/ # Norwegian Bokmål/Nynorsk + return 'no' + when /^sv/ # Swedish variants + return 'sv' + when /^da/ # Danish variants + return 'da' + when /^is/ # Icelandic variants + return 'is' + when /^fi/ # Finnish variants + return 'fi' + when /^fo/ # Faroese variants + return 'fo' + when /^se|smi/ # Sami variants + return 'smi' + end + end + end + + DEFAULT_LANGUAGE end # Root endpoint - returns HTML by default get '/' do - no_response = get_no_response + lang = determine_language(params, request.env) + no_response = get_no_response(lang) case request.accept.first&.to_s when /application\/json/ content_type :json - { answer: no_response, timestamp: Time.now.iso8601 }.to_json + { + answer: no_response, + language: lang, + language_name: get_language_name(lang), + timestamp: Time.now.iso8601 + }.to_json else content_type :html - erb :index, locals: { no_response: no_response } + erb :index, locals: { + no_response: no_response, + language: lang, + language_name: get_language_name(lang), + available_languages: get_available_languages + } end end -# Explicit JSON endpoint -get '/api/no' do +# Languages endpoint - list available languages +get '/languages' do content_type :json { - answer: get_no_response, + languages: get_available_languages, + default: DEFAULT_LANGUAGE, + timestamp: Time.now.iso8601 + }.to_json +end + +# Explicit JSON endpoint with language support +get '/api/no' do + lang = determine_language(params, request.env) + + content_type :json + { + answer: get_no_response(lang), + language: lang, + language_name: get_language_name(lang), timestamp: Time.now.iso8601, service: "No as a Service", - version: "1.0.0" + version: "2.0.0" + }.to_json +end + +# Language-specific endpoint +get '/api/no/:lang' do + lang = params[:lang] + + unless RESPONSES[lang] + content_type :json + status 404 + return { + error: "Language not supported", + requested: lang, + available: get_available_languages.map { |l| l[:code] }, + timestamp: Time.now.iso8601 + }.to_json + end + + content_type :json + { + answer: get_no_response(lang), + language: lang, + language_name: get_language_name(lang), + timestamp: Time.now.iso8601, + service: "No as a Service", + version: "2.0.0" }.to_json end # Health check endpoint get '/health' do content_type :json - { status: "healthy", timestamp: Time.now.iso8601 }.to_json + { + status: "healthy", + languages_loaded: RESPONSES.keys.size, + timestamp: Time.now.iso8601 + }.to_json end # Catch-all route for any other endpoint get '/*' do - no_response = get_no_response + lang = determine_language(params, request.env) + no_response = get_no_response(lang) case request.accept.first&.to_s when /application\/json/ content_type :json - { answer: no_response, timestamp: Time.now.iso8601 }.to_json + { + answer: no_response, + language: lang, + language_name: get_language_name(lang), + timestamp: Time.now.iso8601 + }.to_json else content_type :html - erb :index, locals: { no_response: no_response } + erb :index, locals: { + no_response: no_response, + language: lang, + language_name: get_language_name(lang), + available_languages: get_available_languages + } end end @@ -83,7 +182,7 @@ __END__ @@index - + @@ -107,7 +206,7 @@ __END__ .container { text-align: center; - max-width: 600px; + max-width: 700px; padding: 2rem; background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px); @@ -124,10 +223,19 @@ __END__ .subtitle { font-size: 1.2rem; - margin-bottom: 2rem; + margin-bottom: 1rem; opacity: 0.9; } + .language-info { + font-size: 1rem; + margin-bottom: 2rem; + padding: 0.5rem 1rem; + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; + display: inline-block; + } + .no-response { font-size: 4rem; font-weight: bold; @@ -138,6 +246,16 @@ __END__ background: rgba(255, 255, 255, 0.1); border-radius: 15px; border: 2px solid rgba(255, 107, 107, 0.3); + word-break: break-word; + } + + .controls { + margin: 2rem 0; + display: flex; + flex-wrap: wrap; + gap: 1rem; + justify-content: center; + align-items: center; } .refresh-btn { @@ -157,6 +275,21 @@ __END__ box-shadow: 0 6px 20px rgba(255, 107, 107, 0.4); } + .language-selector { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.3); + color: white; + padding: 0.5rem 1rem; + border-radius: 25px; + font-size: 1rem; + cursor: pointer; + } + + .language-selector option { + background: #444; + color: white; + } + .api-info { margin-top: 2rem; padding: 1rem; @@ -172,6 +305,12 @@ __END__ font-family: 'Courier New', monospace; } + .nordic-flag { + display: inline-block; + margin-left: 0.5rem; + font-size: 1.2em; + } + @media (max-width: 600px) { .container { margin: 1rem; @@ -185,28 +324,63 @@ __END__ .no-response { font-size: 2.5rem; } + + .controls { + flex-direction: column; + gap: 0.5rem; + } }

No as a Service

-

The definitive API for negative responses

+

The definitive multilingual API for negative responses

+ +
+ Currently in: <%= language_name %> + <% if language == 'no' %>🇳🇴<% end %> + <% if language == 'sv' %>🇸🇪<% end %> + <% if language == 'da' %>🇩🇰<% end %> + <% if language == 'is' %>🇮🇸<% end %> + <% if language == 'fi' %>🇫🇮<% end %> + <% if language == 'fo' %>🇫🇴<% end %> +
<%= no_response %>
- +
+ + + +

API Usage

-

JSON API: GET /api/no

-

Accept JSON: curl -H "Accept: application/json" /

+

JSON API: GET /api/no or GET /api/no/:lang

+

Languages: GET /languages

+

URL Parameter: ?lang=<%= language %>

+

Accept-Language header supported

Health Check: GET /health

+ + diff --git a/responses.yml b/responses.yml new file mode 100644 index 0000000..ec737cc --- /dev/null +++ b/responses.yml @@ -0,0 +1,192 @@ +--- +en: + name: "English" + responses: + - "No" + - "Nope" + - "Absolutely not" + - "Not a chance" + - "Never" + - "No way" + - "Negative" + - "Not happening" + - "Nah" + - "No siree" + - "Not today" + - "Dream on" + - "Over my dead body" + - "When pigs fly" + - "Not in a million years" + - "Forget about it" + - "No dice" + - "Not on your life" + - "Fat chance" + - "No can do" + +no: + name: "Norsk (Norwegian)" + responses: + - "Nei" + - "Neida" + - "Aldri" + - "Absolutt ikke" + - "Ikke en sjanse" + - "Glem det" + - "Ikke tale om" + - "Over mitt lik" + - "Aldri i livet" + - "Ikke i dag" + - "Bare glem det" + - "Ikke på din salighet" + - "Når kua flyger" + - "Ikke noe sjanse" + - "Ikke faen" + - "Drøm videre" + - "Negativt" + - "Ikke aktuelt" + - "Ikke på min vakt" + - "Bare drøm" + +sv: + name: "Svenska (Swedish)" + responses: + - "Nej" + - "Nä" + - "Aldrig" + - "Absolut inte" + - "Inte en chans" + - "Glöm det" + - "Inte på tal" + - "Över min döda kropp" + - "Aldrig i livet" + - "Inte idag" + - "Bara glöm det" + - "När grisar flyger" + - "Inte någon chans" + - "Drömma vidare" + - "Negativt" + - "Inte aktuellt" + - "Aldrig någonsin" + - "Inte på min vakttid" + - "Bara dröm" + - "Knappast" + +da: + name: "Dansk (Danish)" + responses: + - "Nej" + - "Nah" + - "Aldrig" + - "Absolut ikke" + - "Ikke en chance" + - "Glem det" + - "Ikke tale om" + - "Over mit lig" + - "Aldrig i livet" + - "Ikke i dag" + - "Bare glem det" + - "Når svin flyver" + - "Ikke nogen chance" + - "Bare drøm" + - "Negativt" + - "Ikke aktuelt" + - "Aldrig nogensinde" + - "Ikke på min vagt" + - "Glem det bare" + - "Nej tak" + +is: + name: "Íslenska (Icelandic)" + responses: + - "Nei" + - "Aldrei" + - "Algjörlega ekki" + - "Engin möguleiki" + - "Gleyma því" + - "Ekki í dag" + - "Aldrei í lífinu" + - "Yfir mína lík" + - "Ekki um það að ræða" + - "Bara gleyma því" + - "Þegar svín fljúga" + - "Engin von" + - "Aldrei nogurra" + - "Bara dreyma" + - "Neikvætt" + - "Ekki í boði" + - "Aldrei aftur" + - "Ekki á mínum degi" + - "Gleyma þessu" + - "Nei takk" + +fi: + name: "Suomi (Finnish)" + responses: + - "Ei" + - "Eipä" + - "Ei koskaan" + - "Ehdottomasti ei" + - "Ei mahista" + - "Unohda se" + - "Ei tule kuuloonkaan" + - "Yli minun ruumiini" + - "Ei ikinä elämässä" + - "Ei tänään" + - "Bara unohda" + - "Kun lehmät lentää" + - "Ei mitään mahdollisuuksia" + - "Haaveile vaan" + - "Negatiivinen" + - "Ei ole ajankohtaista" + - "Ei ikinä" + - "Ei minun vuorollani" + - "Ei tuloa" + - "Kiitos ei" + +fo: + name: "Føroyskt (Faroese)" + responses: + - "Nei" + - "Aldri" + - "Alls ikki" + - "Onki møguleiki" + - "Gleym tað" + - "Ikki í dag" + - "Aldri í lívinum" + - "Yvir mítt lík" + - "Ikki tað" + - "Bara gleym tað" + - "Tá svín fljúgva" + - "Ongin von" + - "Aldri aftur" + - "Bara droyma" + - "Ikki møguligt" + - "Ikki aktuelt" + - "Aldri meir" + - "Ikki á mínum døgum" + - "Gleym hettar" + - "Nei takk" + +smi: + name: "Sámegiella (Northern Sami)" + responses: + - "Ii" + - "Ale goassege" + - "Ale ipmašii" + - "Ii oktage vejolašvuohta" + - "Vajálduhte dan" + - "Ii odne" + - "Ale goassege eallimis" + - "Mu rupmasa ala" + - "Ii leat sáhka" + - "Dušše vajálduhte" + - "Go vuonji liddjo" + - "Ii oktage doaivva" + - "Ale fas" + - "Dušše nágot" + - "Ii vejolašlávvu" + - "Ii leat áigeguovdil" + - "Ale eanet" + - "Ii mu beaivvis" + - "Vajálduhte dán" + - "Ii giitu"