I'm building a web shop and I'm implementing language selection as well as a CMS. I also have to provide an administrator of the site with the means to be able to edit a page through the CMS.
Therein doesn't lay the problem.
The problem is how I should build up my tables for these pages. I've made my database design but didn't think the web pages part through.
I already have the following table structure for the info that is equal throughout all languages for a page (called Webpages) and for language or culture specific info (called Webpages_local).
Which attributes could I add or remove so that I can easily and dynamically perform the CRUD actions?
I'm using MVC4 with razor syntax with the following url structure:
url: "{language}/{controller}/{action}/{id}"
My main concern is now that I'm not sure on how to show the language specific content of a page when a visitor presses, for example, the link to the About page.
Maybe use the controller part of the url and save it as a key in my Webpages table and filter on that as well as the selected language?
So when a visitor goes to http://example.com/nl/About, I in my AboutController I retrieve "nl" and "about", of course filter them first and then with a query to the database select the correct nl content?
How should I go about this technically?
I would use OnActionExecuting to handle the retrieve lang process, something like:
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
#region set Language
var lang = string.Empty;
if (filterContext.RouteData.Values["lang"] != null && !string.IsNullOrWhiteSpace(filterContext.RouteData.Values["lang"].ToString()))
{
// set the culture from the route data (url {lang})
lang = filterContext.RouteData.Values["lang"].ToString();
switch (lang)
{
case "es":
break;
case "en":
break;
default:
lang = "es";//default language
filterContext.RouteData.Values["lang"] = lang;
filterContext.HttpContext.Response.Redirect("/");
break;
}
}
else
{
//set default language
lang = "es";
filterContext.RouteData.Values["lang"] = lang;
}
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang);
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(lang);
#endregion
base.OnActionExecuting(filterContext);
}
Then anywhere in your code you just read the Thread.CurrentThread.CurrentCulture (this will be your global lang indicator) to show the correct language.
UPDATE - know I understand your point =)
If the pages will be dynamically created then you wont have an ActionResult per page, you need just one ActionResult like:
public ActionResult ShowPage(int id,string slug)
{
//Use the slug to check for url attacks and ensure 301 redirections to the correct url
var page = db.Webpages_local.First(p=> p.id == id
&& p.culture.name == Thread.CurrentThread.CurrentCulture);
return View(page);
}
For SEO reasons i would suggest you define a route like:
routes.MapRoute(
name: "LocalizedPages",
url: "{lang}/p/{slug}/{id}",
defaults: new { controller = "Page", action = "Show", id = UrlParameter.Optional },
constraints: new { lang = #"(es|en|fr|nl)" }
);
That give you urls like:
/nl/p/about/1 //the p is just an identifier for 'page', to differentiate this routes from others
I would add a column called language or similar to your table instead of having multiple tables.
Then your controller can fetch the right page after finding the requested language key in your menu table and then it can read the content in your webpages table.
Related
Hi i am working on a project in laravel 7.0, in back-end i have a table called Posts which contains 2 text language input one in french and the other is arabic added by the back-end application.
what i am trying to do is when the user uses the French Language i want the title_fr to be displayed on the view and same thing in Arabic language the title should be title_ar.
P.S data are stored in French and Arabic
I have tried the similar solution given in an other similar question but none of it worked in my case!
Any idea how i might get this to work ?
Thanks in advance.
You can do something similar to below. We have a model Post, this model has an attribute title. I also assume that you have an attribute that will return user's language from the User model.
class Post extends Model
{
public function getTitleAttribute(): string
{
return Auth::user()->language === 'fr' ? $this->title_fr : $this->title_ar;
}
}
FYI above is just a demo on what can be done. For a full blow solution I would recommend decorator pattern.
Also it might be worth considering using morph for things like that. You can have a service provider that will initiate the morph map for you post model relevant to the language that user has, I.e.
Class ModelProvider {
Protected $models = [
‘fr’ => [
‘post’ => App/Models/Fr/Post::class,
],
‘ar’ => [
‘post’ => App/Models/Ar/Post::class,
]
];
Public function boot() {
$language = Auth::user()->Settings->language;
Relation::morphMap($This->models[$language]);
}
}
Afterwards you just need to call to Relation::getMorphModel(‘post’) to grab Post class that will return correct language.
I.e. App/Models/Fr/Post can have a an attribute title:
Public function getTitleAttribute(): string {
Return $this->title_fr;
}
For example above you would also want to utilise interfaces to make sure that all models follow the same contract, something below would do the trick:
Interface I18nPostInterface {
Public function getTitleAttribute(): string
}
Also, depending on the database you use, to store titles (and other language data) in a JSON format in the database. MySQL 8 has an improve support for JSON data, but there are limitations with that.
So I was Able to fetch data from my database based on the Language selected by the user.
Like i said before I have a table called Posts and has columns id,title_fr and title_ar. I am using laravel Localization.
Inside my PostController in the index function i added this code:
public function index()
{
//
$post = Post::all();
$Frtitle = post::get()->pluck('title_fr');
$Artitle = post::get()->pluck('title_ar');
return view('post.index',compact('post','Frtitle','Artitle'));
}
if anyone has a better way then mine please let me know, i am sure
there is a better way.
The "official" solution for including images in visualforce email templates suggests hard coding IDs in your template to reference an image file stored as a document.
https://help.salesforce.com/HTViewHelpDoc?id=email_template_images.htm&language=en_US
Is there a better way that avoids hard coding instance ID and OID? I tried using the partner URL to grab the instance ID, but I got the following error
Error Error: The reference to entity "oid" must end with the ';' delimiter.
Using:
{!LEFT($Api.Partner_Server_URL_140,FIND(".com/",$Api.Partner_Server_URL_140)+3)/
to replace "https://na2.salesforce.com/"
in
"na2.salesforce.com/servlet/servlet.ImageServer?id=01540000000RVOe&oid=00Dxxxxxxxxx&lastMod=1233217920"
Should I use a static resource instead?
I've arrived here looking for an answer for this question related to hardcoded ID and OID in Visualforce e-mail templates. Well, I found a workaround for that.
First I needed to create a Visualforce Component:
<apex:component access="global" controller="LogomarcaController">
<apex:image url="{!LogoUrl}" />
</apex:component>
In the respective controller class, I've created a SFInstance property to get the correct URL Salesforce Instance, LogoUrl property to concatenate SFInstance and IDs... And Finally I've used Custom Settings (Config_Gerais__c.getInstance().ID_Documento_Logomarca__c) to configurate the ID of Image (in my case, Document Object) on Sandbox or Production:
public class LogomarcaController {
public String LogoUrl {
get {
id orgId = UserInfo.getOrganizationId();
String idDocumentoLogomarca = Config_Gerais__c.getInstance().ID_Documento_Logomarca__c;
return this.SfInstance + '/servlet/servlet.ImageServer?id=' + idDocumentoLogomarca + '&oid=' + orgId ;
}
}
public String SfInstance
{
get{
string SFInstance = URL.getSalesforceBaseUrl().toExternalForm();
list<string> Dividido = SFInstance.split('.visual', 0);//retira o restante a partir de .visual
SFInstance = dividido[0];
dividido = SFInstance.split('://',0);//retira o https://
SFInstance = dividido[1];
if(!SFInstance.contains('sybf')) //managed package prefix, if you need
{
SFInstance = 'sybf.'+ SFInstance;
}
return 'https://'+SFInstance;
}
}
}
And finally, I've added the component in Visualforce template:
<messaging:emailTemplate subject="Novo Ofício - {!relatedTo.name}" recipientType="User" relatedToType="Oficio__c" >
<messaging:htmlEmailBody >
<c:Logomarca />
</messaging:htmlEmailBody>
<messaging:plainTextEmailBody >
</messaging:plainTextEmailBody>
</messaging:emailTemplate>
PS: Some of my variables, properties and comments are in my native language (portuguese). If you have some problems understanding them, please ask me!
We ran into a similar problem and after trying various solutions, the following worked for us. In our case the image is uploaded as a content asset(https://help.salesforce.com/articleView?id=000320130&type=1&language=en_US&mode=1)
Solution:
<img src="{!LEFT($Api.Partner_Server_URL_260,FIND('/services',$Api.Partner_Server_URL_260))}/file-asset-public/<Image_Name_Here>?oid={!$Organization.Id}&height=50&width=50"/>
I've created a small module that I would like to link account notes with users.
I have written the beginnings of the module that includes a form for adding notes and date created. This works when I access mydomain.com/admin/user_notes.
My question is, how do I get this form to display in the admin section of the site on a users orders history page. eg mydomain.com/users/1245/order-history
I would like our admins who have a specific role to be able to add notes when they view a users order history page.
Thanks in advance for any advice.
you could use a block, create one with the hook_block_info and hook_block_view function.
Like this:
function tips_block_info() {
$block['yourBlockName'] = array(
'info' => t('This my created block'),
'cache' => DRUPAL_NO_CACHE, // Disable caching if you need/want to
);
return $block;
}
Thisby will create an empty block, to fill it with content use hook_block_view:
function moduleName_block_view($delta = '') {
$block = array();
switch($delta) {
// The delta of your block will be the key from the $block array we set in hook_block_info
case 'yourBlockName':
// Set the block title
$block['subject'] = 'Hey I\'m your block title';
$block['content'] = 'Block content goes here can also be the output of any function';
break;
}
return $block;
}
Don't forget to set access permission for your block if you need any, you can do that by editing the block on the block admin page.
Relevant Drupal API links:
hook_block_info
hook_block_view
i have a situation here that i can't seem to figure out. Please if anybody knows how to resolve this i would love to hear suggestions.Thanks
I have a "global view" that is visible on a subnavbar in the app, that is a calendar, this calendar serves as a global calendar throughout the application, so almost all the views use the calendar view & model to set show data according to the date selected.
This calendar view/model should have some way to store in history each time the date is changed, this (i think) is done using a single URL or query string parameters each time the date is changed, something like
webapp/view1?date=20120301
and when the date is changed, so its the query string.
I would like to use query string parameters for this so i don't have to specify on each route the (/:date) optional parameter.
THE THING IS backbone stopped firing a route change or a history event when query strings are changed, they simply ignore query strings on the Backbone.History implementation, this is breaking all my implementation cause i can't track each time the querystring is changed, so the "back" button will not fire a change event and therefore i can't change the date on the model that would change the date on the calendar.
I know a simple solution to this would be just using "pretty URL" and adding that date parameter to each view, but im trying to avoid that.
Any suggestions?
Thanks in advance
UPDATE
I ended up using "pretty URLs" like the Backbone documentation suggests, cause using query strings would bring me a lot of trouble for tracking the URL change and history, also wouldn't work as expected when using hashchange instead of pushState.
So, my code ended up like this:
Attaching somewhere in your router, view, controller, something, to the "route" event of your router, to check the URI for the date and set this date to the calendar picker:
this.listenTo(myRouter, "route", this.routeChanged);
Then, this method would do something like:
checkURIdateParameter: function (route, arr) {
var found = false;
for (var i = 0; i < arr.length ; i++) {
if (arr && arr[i] && arr[i].indexOf("date=") != -1) {
if (app.models.dateControl) {
var dateFromParameter = new Date(arr[i].substring("date=".length).replace(/\$/g, ":"));
dateFromParameter = (isNaN(dateFromParameter.getTime())) ? app.models.dateControl.defaults.date : dateFromParameter;
app.models.dateControl.set("date", dateFromParameter);
found = true;
}
}
}
if (!found) app.models.dateControl.set("date", app.models.dateControl.defaults.date, {changeURI:false});
}
This serves as the function that will read params from the URI and reflect those changes on the dateControl.
There should be another method that will be the one in charge of updating the URI to a new one each time the date is changed (so that the params are in sync with the view) and the link can be copied and pasted with no problems.
Somewhere on your router, view, controller:
this.listenTo(app.models.dateControl, "change:date", this.updateURIdateParameter);
This is a method that is attached to a model that has the current date, this model is updated by the calendar picker (the UI control) or the method that was linked with the route event (routeChanged, look above).
This method should do something like this:
, updateURIdateParameter: function (a, b, c) {
if (c && c.changeURI == false) return; //this is in case i don't want to refresh the URI case the default date is set.
var currentFragment = Backbone.history.fragment;
var newFragment = "";
var dateEncoded = app.models.dateControl.get("date").toJSON().replace(/\:/g, "$");
newFragment = currentFragment.replace(/\/date=([^/]+)/, "") + "/date=" + dateEncoded;
if (newFragment != currentFragment) {
app.router.navigate(newFragment, false);
}
}
This method gets the currentDate selected from the corresponding model, parses it, then takes the URL from the Backbone.history.fragment, execs a nice regexp against it that will replace the old date parameter (if exists) and then appends the new encoded date.
Then navigates to this route in silent mode so that the method that checks the route is not called (this prevents annoying loops).
I hope this helps.
I would suggest using "Pretty URL".
FYI Page URL in the browser bar will blink in this example.
Somewhere inside your Backbone.Router:
this.route('*action', function() {
console.log('404');
});
this.route(/^(.*?)\?date=(\d+)$/, function(route, date) {
// same current route (with ?date)
var fragment = Backbone.history.fragment;
// navigate to main route (to create views etc.)
this.navigate(route, {trigger:true, replace:true});
// silent change hash
this.navigate(fragment);
});
this.route('any', function() {
// need wait for hash change
_.defer(function() {
console.log('parse date here and do what you want', window.location.hash.match(/^(.*?)\?date=(\d+)$/));
});
});
this.route('route', function() {
// need wait for hash change
_.defer(function() {
console.log('parse date here and do what you want', window.location.hash.match(/^(.*?)\?date=(\d+)$/));
});
});
I have set up the Display Mode in Application Start event as
DisplayModeProvider.Instance.Modes.Insert( 0, new DefaultDisplayMode( "iPhone" ){
ContextCondition = ( context =>
context.GetOverriddenUserAgent( ).IndexOf(
"iPhone",
StringComparison.OrdinalIgnoreCase ) >= 0 ) } );
Then in the controller I have return View where I specify the view name:
return View( "~/Views/Common/User/Login.cshtml", viewModel );
And if I visit the page from the iPhone it will go directly to Login View
If I do not specify the view name:
return View( viewModel );
In this case from the iPhone I see the Login.iPhone.cshtml
Question: Is it possible to specify the name of the view but some how the DisplayModeProvider will select general or iPhone version of the cshtml file?
I don't normally like to resurrect old questions but as this one was never answered and this is one I had particular trouble finding an answer to myself, it may be worth having an answer for anyone else who comes across the same problem.
You could add your additional locations to the ViewLocationFormats and PartialViewLocationFormats collections for the ViewEngines you are using. This way you could just specify the view name as tvanfosson suggests and MVC would find the file correctly, which should allow the mobile overriding to work it's magic.
Here is some code I use to override the PartialViewLocationFormats, you could also do the same using ViewLocationFormats. This is added in global.asax as part of application_start
ViewEngines.Engines.Clear();
var razorViewEngine = new RazorViewEngine
{
PartialViewLocationFormats = new[]
{
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml",
"~/Views/{1}/EditorTemplates/{0}.cshtml",
"~/Views/{1}/DisplayTemplates/{0}.cshtml",
"~/Views/Shared/DisplayTemplates/{0}.cshtml"
}
};
Because this method involves clearing the viewengines collection, you will need to add in all locationFormats, even the standard ones, for all the view engines you are using.