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; + } }The definitive API for negative responses
+The definitive multilingual API for negative responses
+ +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